Цель данного задания:
Сначала установим нужные нам версии библиотек. Мы гарантируем, что в данных версиях задание будет корректно отрабатывать.
После установки нужных версий, возможно, нужно перезагрузить среду (runtime), но скорее всего вам это не понадобится
На скачивание файла и установку понадобится не более 5 минут.
Важно! Устанавливать нужные версии нужно каждый раз, когда создается новый рантайм. Например, если вы 2 часа подряд делаете это задание, то подготовить библиотеки достаточно 1 раз. Но если вы, например, начали в понедельник, затем закрыли/выключили ноутбук, то при продолжении в среду, вам нужно будет запустить рантайм заново и следовательно заново установить библиотеки.
Важно! Если вы предпочитаете делать практические задания на своем личном ноутбуке, то проверьте, что вы установили рабочее окружение в соответствии с гайдом
! gdown 1pIw8GdGKY6fZ_XNPc6snimdV6lbXJ199
! pip install -r /content/requirements_small.txt
import catboost
assert(catboost.__version__ == '1.2.1')
Теперь можно приступать к выполнению задания! :)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
from copy import copy
Если Вы дальтоник, то можете воспользоваться готовой colormap из matplotlib (или найти свою):
plt.style.use('tableau-colorblind10')
from matplotlib.colors import ListedColormap
from sklearn.svm import LinearSVC, SVC
from sklearn.decomposition import PCA
from sklearn.model_selection import cross_val_score, cross_val_predict, cross_validate, train_test_split
from sklearn.metrics import make_scorer, accuracy_score, roc_auc_score
from sklearn.datasets import make_blobs, make_circles, make_moons
def make_moons_cls(size=1000, d=2):
X, y = make_moons(n_samples=size, noise=0.15)
if d > 2:
X = np.concatenate((X, np.random.normal(size=(size, d-2))), axis=1)
return X, y
def make_circles_cls():
X, y = make_circles()
def generate_data_with_imb_classes(size1=100, size2=10):
X = np.r_[(
np.random.normal(loc=1.0, size=(size1, 2)),
np.random.normal(loc=0.5, size=(size2, 2))
)]
y = np.ones(len(X))
y[-size2:] = 0
return X, y
def plot_separating_surface(X, y, cls, view_support=False, title=''):
x_min = min(X[:, 0]) - 0.1
x_max = max(X[:, 0]) + 0.1
y_min = min(X[:, 1]) - 0.1
y_max = max(X[:, 1]) + 0.1
h = 0.005
cm = plt.cm.RdBu
cm_bright = ListedColormap(['#FF0000', '#0000FF'])
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
np.arange(y_min, y_max, h))
Z = cls.predict(np.c_[xx.ravel(), yy.ravel()])
plt.figure(figsize=(10, 10))
if title:
plt.title(title)
plt.scatter(X[:, 0], X[:, 1], c=y, edgecolors='k', s=40, cmap=cm_bright)
if view_support:
plt.scatter(X[cls.support_, 0], X[cls.support_, 1],
c=y[cls.support_], edgecolors='k', s=150, cmap=cm_bright)
Z = Z.reshape(xx.shape)
plt.xticks(())
plt.yticks(())
plt.contourf(xx, yy, Z, cmap=cm, alpha=.3)
plt.show()
import gdown
gdown.download_folder('https://drive.google.com/drive/folders/1bp39_Jj0edo1lzxZ3DIoChsOVy5DVi1R?usp=sharing')
В ячейке ниже генерируется выборка, состоящая из объектов двух классов. Каждый объект представлен двумя координатами, так что объекты этой выборки можно отобразить на плоскости, используя функцию scatter из библиотеки matplotlib.
В этом задании вам надо будет обучить линейную разделяющую поверхность с помощью $\mbox{sklearn.svm.SVC(kernel='linear')}$, а также нелинейную c rbf-ядром с помощью $\mbox{sklearn.svm.SVC(kernel='rbf')}$. Остальные параметры методов можете оставить дефолтными. Делить выборку на обучение и валидацию сейчас не требуется, так как нас будет пока интересовать только форма разделяющей кривой.
X, y = make_moons_cls()
linear_svc = SVC(kernel='linear')
nonlinear_svc = SVC(kernel='rbf')
Визуализируем выборку
plt.figure(figsize=(8, 6))
plt.scatter(X[y == 0, 0], X[y == 0, 1], label='y=0')
plt.scatter(X[y == 1, 0], X[y == 1, 1], label='y=1')
plt.xlabel('$x_0$')
plt.ylabel('$x_1$')
plt.legend()
plt.grid()
Обучите модели и визуализируйте разделяющую поверхность для обеих моделей с помощью функции plot_separating_surface(). Посчитайте точность (accuracy) на обучающей выборке для каждой из моделей.
linear_svc.fit(X, y)
plot_separating_surface(X, y, linear_svc, view_support=True, title="Linear")
nonlinear_svc.fit(X, y)
plot_separating_surface(X, y, nonlinear_svc, view_support=False, title="Non-Linear")
print(f'Train Accuracy (linear): {accuracy_score(y, linear_svc.predict(X)):.3f}')
print(f'Train Accuracy (non linear): {accuracy_score(y, nonlinear_svc.predict(X)):.3f}')
Train Accuracy (linear): 0.877 Train Accuracy (non linear): 0.991
Сделайте вывод о получившихся результатах. Какая из моделей лучше подходит для данной выборки и почему?
Вывод: Модель с нелинейным ядром rbf оказывается более предпочтительной для данной выборки, поскольку обучающие данные линейно неразделимы. Этот вывод основан как на визуализации разделяющих поверхностей, так и на точности классификации.
Продолжаем работать с выборкой и моделями из первой части. Для линейной и rbf-моделей рассмотрим опорные объекты, полученные после обучения. Визуализировать их можно, используя функуцию plot_separating_surface с параметром vissupport=True. Достанем опорные объекты из обученной модели с помощью поля model.support.
plot_separating_surface(X, y, nonlinear_svc, view_support=True)
Пункт 1 Обучим новые две модели $\mbox{SVC(kernel='rbf')}$, используя только опорные объекты построенные с помощью соответственно линейной (linear_svc) и нелинейной (nonlinear_svc) моделей из первой части.
# Учим модели только на опорных объектах
svc_on_linear_support = SVC(kernel='rbf').fit(X[linear_svc.support_, :], y[linear_svc.support_])
svc_on_rbf_support = SVC(kernel='rbf').fit(X[nonlinear_svc.support_, :], y[nonlinear_svc.support_])
plot_separating_surface(X, y, svc_on_linear_support)
plot_separating_surface(X, y, svc_on_rbf_support)
Задание: Сравните полученные разделяющие поверхности с нелинейной моделью (nonlinear_svc) из первой части. Какая из поверхностей больше похожа на нелинейнную модель из первой части и почему, опишите в выводе.
Вывод: Модель, обученная на опорных векторах нелинейной модели, демонстрирует большую схожесть с самой нелинейной моделью, так как её обучение происходит на тех же опорных векторах, на основе которых строилась исходная нелинейная модель.
Пункт 2 Обучим модель $\mbox{SVC(kernel='rbf')}$, используя все объекты кроме тех, что являлись опорными для нелинейной модели из первой части (nonlinear_svc) и сравним эту модель вместе с svc_on_rbf_support с нелинейной моделью из первой части (nonlinear_svc). Визуализируйте разделяющие поверхности обеих моделей.
non_support_vectors = [i for i in range(len(X)) if i not in nonlinear_svc.support_]
svc_all_without_rbf_support = SVC(kernel='rbf').fit(X[non_support_vectors, :], y[non_support_vectors])
plot_separating_surface(X, y, svc_all_without_rbf_support)
plot_separating_surface(X, y, nonlinear_svc)
plot_separating_surface(X, y, svc_on_rbf_support)
Сделайте вывод: Сильно ли полученные поверхности отличаются от той, что была получена в первой части? Что произошло с пограничными объектами? Объясните полученные результаты.
Вывод: Полученные поверхности практически не различаются от тех, которые были представлены в первой части. Некоторые граничные объекты были исключены, поскольку они служили опорой. Также некоторые из этих граничных объектов были переклассифицированы в другой класс в результате удаления опорных объектов, находившихся ближе к границе. Когда эти опорные объекты были убраны, граница сдвинулась ближе к оставшимся объектам. Также были удалены объекты, которые были ошибочно классифицированы, поскольку они также являлись опорными.
Формы разделяющих поверхностей могут быть вариативными для нелинейного случая. Иногда, выбранная форма поверхности может плохо подходить для целевого распределения объектов. Особенно это может быть заметно, если соотношение классов в обучении отличается от тестового. Такое свойственно медицинским данным, где в обучающих данных часто наблюдается перекос в сторону больных, так как именно их данные чаще собираются.
Давайте обучим SVC на несбалансированных данных и построим разделяющую поверхность для тестовой выборки с другим соотношением классов.
X_distr1 = np.load('05-SVM/imbalanced/X_imb.npz.npy')
y_distr1 = np.load('05-SVM/imbalanced/y_imb.npz.npy')
X_distr2 = np.load('05-SVM/imbalanced/X_imb_test.npz.npy')
y_distr2 = np.load('05-SVM/imbalanced/y_imb_test.npz.npy')
# Модель с дефолтными параметрами, которую Вам предстоит улучшить
base_model = SVC()
base_model.fit(X_distr1, y_distr1)
plot_separating_surface(X_distr2, y_distr2, base_model)
Так как синих объектов было существенно больше в обучении, разделяющая поверхность отнесла к этому классу большую часть пространства около границы классов. При этом, из-за возросшего количества красных объектов в тестовой выборке, многие из них стали ошибочно относиться к другому классу.
Один из способов исправить эту проблему - это повлиять на форму поверхности с помощью задания весов классов, которые задаются через параметр class_weight в sklearn.svm.SVC. Особенно это может быть полезно в задачах, где известно, что распределение классов в обучающей выборке отличается от реального.
В данном задании вам будет дана выборка с несбалансированными данными. Кроме того, дана вторая выборка, в которой классы имеют то же распределение, но классы имеют другое соотношение. Вам нужно построить различные rbf-модели, меняя параметры весов классов и визуализировать разделяющие поверхности. Попробуйте улучшить качество на второй выборке (X_distr2, y_distr2), обучаясь только на первой (X_distr1, y_distr1) меняя параметры весов классов относительно дефолтных: class_weight={1: 1.0, 0: 1.0}. В качестве метрики, которую нужно оптимизировать нужно использовать accuracy на (X_distr2, y_dist2).
from itertools import product
w1_best, w2_best = None, None
best_acc = None
grid = [0.01, 0.1, 0.5, 1.0, 5, 10, 100, 1000]
for w1, w2 in product(grid, grid):
svc_cls = SVC(C=1.0, class_weight={1: w1, 0: w2})
svc_cls.fit(X_distr1, y_distr1)
acc = accuracy_score(y_distr2, svc_cls.predict(X_distr2))
# Save model with best accuracy
if best_acc is None or best_acc < acc:
best_acc = acc
w1_best, w2_best = w1, w2
print(f'Weight1: {w1_best},\nWeight2: {w2_best},\nBest accuracy: {best_acc}')
Weight1: 0.1, Weight2: 5, Best accuracy: 0.9666666666666667
best_svc_cls = SVC(C=1.0, class_weight={1: w1_best, 0: w2_best})
best_svc_cls.fit(X_distr1, y_distr1)
plot_separating_surface(X_distr2, y_distr2, best_svc_cls)
mb = SVC(class_weight="balanced")
mb.fit(X_distr1, y_distr1)
plot_separating_surface(X_distr2, y_distr2, mb)
print('My best classifier:')
print('Accuracy:', accuracy_score(y_distr2, best_svc_cls.predict(X_distr2)))
My best classifier: Accuracy: 0.9666666666666667
Возможность строить нелинейные поверхности может сильно улучшить качество, но и несет риск переобучения. В этом задании предстоит обучить лучшую svm модель и получить хорошее качество на тесте в системе тестирования. Для контроля переобучения рекомендуется пользоваться кросс-валидацией. Для улучшения качества рекомендуется подбирать
Также не забывайте, что при решении задач машинного обучения полезно смотреть в данные :)
Все csv-таблицы с данными вы можете взять из публичного теста, который также есть в проверяющей системе. Для этого распакуйте архив с публичными тестами и положите файлы в рабочей директории (рядом с ноутбуком)
X_train = np.load('05-SVM/public/cX_train.npy')
y_train = np.load('05-SVM/public/cy_train.npy')
X_test = np.load('05-SVM/public/cX_test.npy')
X_train.shape, y_train.shape, X_test.shape
((800, 5), (800,), (200, 5))
X = X_train
y = y_train.ravel()
plt.scatter(X[y == 0, 4], X[y == 0, 3], label='y=0')
plt.scatter(X[y == 1, 4], X[y == 1, 3], label='y=1')
plt.legend()
<matplotlib.legend.Legend at 0x7ee1a8d62ef0>
Отправьте код обучения модели с оптимальными параметрами в проверяющую систему, воспользовавшись приложенным шаблоном svm_solution.py. Кросс-валидацию параметров в посылаемом решении делать не нужно -- достаточно подобрать, например, их тут, а в решении уже обучать модель с оптимальными параметрами.
В предыдущей части Вы обучили хорошую SVM модель, подбирая гиперпараметры модели. Давайте теперь попробуем обучить логистическую регрессию на этой же выборке, и по кросс-валидации оценить влияние гиперпараметров на линейную модель.
from sklearn.model_selection import GridSearchCV
from sklearn.linear_model import LogisticRegression
X_train = np.load('05-SVM/public/cX_train.npy')
X_test = np.load('05-SVM/public/cX_test.npy')
y_train = np.load('05-SVM/public/cy_train.npy')
model = GridSearchCV(LogisticRegression(max_iter=100, n_jobs=4), {'C' : np.linspace(0.01, 4, 100)}, cv=5, scoring='accuracy', n_jobs=-1)
model.fit(X_train, np.ravel(y_train))
print("Best accuracy: ", model.best_score_)
print("Best parameter: ", model.best_params_)
Best accuracy: 0.78125
Best parameter: {'C': 0.4130303030303031}
model_2 = GridSearchCV(SVC(kernel='rbf'), {'kernel' : ['poly', 'sigmoid', 'rbf'], 'C' : np.linspace(0.01, 4, 100)}, cv=5, scoring='accuracy', n_jobs=-1)
model_2.fit(X_train, np.ravel(y_train))
print("Best accuracy: ", model_2.best_score_)
print("Best parameter: ", model_2.best_params_)
Best accuracy: 0.91875
Best parameter: {'C': 1.6221212121212123, 'kernel': 'rbf'}
Сделайте выводы о влиянии выбора гиперпараметров на качество обучения линейной и SVC моделей. Также опишите, какие преобразования выборки/подбор каких гиперпараметров помогли добиться высокого качества на кросс-валидации в данной задаче.
Вывод: В контексте логистической регрессии, ключевую роль играет коэффициент регуляризации, в то время как SVM предоставляет обширный набор гиперпараметров, включая различные типы ядер, их параметры, а также параметр отступа. Качество логистической регрессии можно улучшить, прибегая к техникам, аналогичным тем, что используются в SVM, таким как явные трансформации данных, создание новых признаков и переход в различные спрямляющие пространства, например, пространство полиномов степени n. Нормализация или стандартизация признаков также могут оказаться полезными. SVM, использующий ядра, неявно модифицирует пространство, в котором происходит оптимизация разделяющей гиперплоскости. В отличие от логистической регрессии, SVM предоставляет больше вариаций новых пространств, и этот процесс почти не требует дополнительных вычислительных ресурсов. В общем, применение SVM может обеспечить более высокое качество с точки зрения метрик, однако с точки зрения интерпретации результатов логистическая регрессия остается более предпочтительной.
В предыдущих заданиях мы убедились в мощности и гибкости моделей SVM. Теперь ответим на вопрос, насколько реально обучить модель SVM на выборках большого размера или с большим числом признаков.
Нужно провести два эксперимента. В первом перебирать размер выборки и для каждого запуска посчитать реальное время обучения модели. При этом делить выборку на обучение и тестирование не нужно. Также качество обученной модели в данном эксперименте не имеет значение. Размеры выборки предлагается перебирать в диапазоне range(1000, 10001, 1000) с использованием generate_data_with_balanced_classes(size=n).
Необходимо сравнить время обучения SVM с логистической регрессией. Для этого замеры повторите также для модели sklearn.linear_model.LogisticRegression. Время обучения одной модели замеряйте с помощью стандартной библиотеки time (пример в ячейке ниже).
Вы можете поставить эксперименты и с большими выборками, чем предлагается в задании (сгенерировать их),
тогда эффект должен быть виден еще сильнее.
Внимание! во время замеров времени работы, отключите сторонние процессы, занимающие CPU, иначе замеры времени работы окажутся некорректными. Помните, что времени работы в зависимости от числа данных и признаков должно меняться монотонно, без ступенчатых изменений (за исключением небольшого шума).
# Как замерять время
import time
time_start = time.time()
time.sleep(1) # Вместо этой команды - запуск замеряемого алгоритма
print("Время работы:", time.time() - time_start)
Время работы: 1.001183271408081
def generate_data_with_balanced_classes(size=500, d=2, noise_scale=0.1):
X = np.random.normal(size=(size*2, d))
mask = X[:, 1] ** 2 > X[:, 0] - 0.1 + np.random.normal(scale=noise_scale)
y = np.ones(len(X))
y[mask] = 0
return X, y
s, r = [], []
for n in range(1000, 10001, 1000):
X, y = generate_data_with_balanced_classes(size=n)
start = time.time()
SVC().fit(X, y)
s.append(time.time() - start)
start = time.time()
LogisticRegression().fit(X, y)
r.append(time.time() - start)
Во втором эксперименте предлагается проделать то же самое, что и в первом эксперименте, только меняя размерность пространства признаков. Для этого можете воспользоваться функцией generate_data_with_balanced_classes(dim=d). Признаки предлагается перебирать по сетке $range(10, 1001, 100)$.
s_2, r_2 = [], []
for n in range(10, 1001, 100):
X, y = generate_data_with_balanced_classes(size=n)
start = time.time()
SVC().fit(X, y)
s_2.append(time.time() - start)
start = time.time()
LogisticRegression().fit(X, y)
r_2.append(time.time() - start)
Постройте графики времени работы в зависимости от числа объектов для SVM и логистической регрессии, сравните их и сделайте выводы.
Придумывая обоснование получившимся результатам попробуйте использовать вид решаемой задачи в SVM, который был дан вам на лекции.
plt.plot(range(1000, 10001, 1000), s, color='red', label='SVM')
plt.plot(range(1000, 10001, 1000), r, color='blue', label='Regression')
plt.xlabel('N-dimension')
plt.ylabel('Run time')
plt.grid(True)
plt.legend()
plt.show()
plt.plot(range(10, 1001, 100), s_2, color='red', label='SVM')
plt.plot(range(10, 1001, 100), r_2, color='blue', label='Regression')
plt.xlabel('N-dimension')
plt.ylabel('Run time')
plt.grid(True)
plt.legend()
plt.show()
Вывод: Обучение метода опорных векторов представляет собой оптимальное решение дуальной задачи, за которым следует преобразование к исходной задаче. Этот процесс включает в себя применение методов градиентного спуска, что требует больше времени по сравнению с обучением логистической регрессии. Поскольку метод опорных векторов решает задачу оптимизации, время обучения увеличивается, особенно при сопоставлении с логистической регрессией, которая, в основном, обучается с использованием стохастического градиентного спуска. Эффективность обучения метода опорных векторов существенно зависит от размера и размерности обучающей выборки. При увеличении этих параметров увеличивается как количество, так и размерность опорных векторов, поскольку требуется их хранение.
Если уменьшение числа объектов сложная и зачастую невозможная задача, то для понижения числа признаков существует стандартное решение. В предыдущих заданиях Вы уже сталкивались с l1-регуляризацией, которая позволяла уменьшить число признаков в задаче линейной классификации/регрессии. Однако для большинства ML-алгоритмов такой способ уменьшения числа признаков неприменим.
Зато существует стандартное для всех алгоритмов понижение размерности входа. Данный алгоритм называется Principal Component Analysis (PCA, метод главных компонент). Он находит такое линейное пространство меньшей размерности $k$ ($k << d$, где d изначальная размерность входа), проекция на которое теряет меньше всего информации. Подробнее об этом можете почитать тут https://scikit-learn.org/stable/modules/decomposition.html#pca.
Эксперимент: Проекция в очень малое число координат сильно "упрощает" выборку, из-за чего качество решения задачи может в итоге упасть. В следующем эксперименте предлагается исследовать зависимость скорости работы метода и качества решения задачи при использовании понижения размерности. Требуется построить два графика:
При этом под полным циклом обучения подразумевается обучение PCA + обучение SVM. Данные для обучения: первые две координаты - луны, которые были в первом задании, а остальные координаты, случайные. Таким образом, без понижения размерности SVM с rbf ядром должен иметь точность близкую к 100\%. Чтобы лучше видеть эффект на графиках, можете менять размер генерируемой выборки. Для данных размеров $k$ рекомендуется перебирать от 10 до 500 (тогда будет видна требуемая закономерность).
P.S. Не забывайте делить выборку на обучение и валидацию в этом эксперименте (так как мы смотрим на качество, мы хотим считать его честно). При этом PCA как и любой другой алгоритм ML тоже нельзя учить на тесте. Общая схема применения PCA описана в ячейках ниже.
X_moons, y_moons = make_moons_cls(2000, 1000)
X_moons.shape
(2000, 1000)
plt.scatter(X_moons[:, 0], X_moons[:, 1], c=y_moons)
plt.xlabel('$x_0$')
plt.ylabel('$x_1$')
plt.show()
plt.scatter(X_moons[:, 0], X_moons[:, 2], c=y_moons)
plt.xlabel('$x_0$')
plt.ylabel('$x_2$')
plt.show()
plt.scatter(X_moons[:, 2], X_moons[:, 3], c=y_moons)
plt.xlabel('$x_2$')
plt.ylabel('$x_3$')
plt.show()
Казалось бы понизить размерность в этой задаче достаточно легко, нужно просто выбрать только первые две координаты. Однако из-за нелинейности разделяющей поверхности, для PCA это задача нетривиальна. Такая же ситуация наблюдается и в большинстве прикладных задач. Теперь переходите к эксперименту :)
# Пример правильного обучения PCA с делением на train/test
pca_model = PCA(n_components=500)
X_moons_train, X_moons_test, y_moons_train, y_moons_test = train_test_split(
X_moons, y_moons, test_size=0.2) # делим выборку на трейн тест для оценки качества всего алгоритма
X_train_for_pca, X_train_for_svc, y_train_for_pca, y_train_for_svc = train_test_split(
X_moons_train, y_moons_train, test_size=0.5) # делим выборку на трейн тест для оценки качества всего алгоритма
print(X_moons.shape, y_moons.shape)
print(X_moons_train.shape, y_moons_train.shape)
print(X_train_for_pca.shape, y_train_for_pca.shape)
# Учим PCA
pca_model.fit(X_train_for_pca)
# Применяем PCA
X_moons_test_transformed = pca_model.transform(X_moons_test)
X_train_for_svc_transformed = pca_model.transform(X_train_for_svc)
print(X_train_for_svc_transformed.shape)
# Учим SVC (на другой выборке чтобы не переобучиться)
svc_on_transformed = SVC(kernel='rbf')
svc_on_transformed.fit(X_train_for_svc_transformed, y_train_for_svc)
preds = svc_on_transformed.predict(X_moons_test_transformed)
print('Accuracy after PCA:', accuracy_score(preds, y_moons_test))
(2000, 1000) (2000,) (1600, 1000) (1600,) (800, 1000) (800,) (800, 500) Accuracy after PCA: 0.575
t, a = [], []
for n in range(10, 501, 10):
pcam = PCA(n_components=n)
start = time.time()
pcam.fit(X_train_for_pca)
t.append(time.time() - time_start)
svct = SVC()
svct.fit(pcam.transform(X_train_for_svc), y_train_for_svc)
a.append(accuracy_score(y_moons_test, svct.predict(pcam.transform(X_moons_test))))
plt.plot(range(10, 501, 10), t)
plt.xlabel('N-dimension')
plt.ylabel('Run time')
plt.grid()
plt.show()
plt.plot(range(10, 501, 10), a)
plt.xlabel('N-dimension')
plt.ylabel('Accuracy')
plt.grid()
plt.show()
Опишите в выводе как ведет себя качество решения задачи и время работы в зависимости от числа компонент в PCA.
Вывод: Чем больше компонентов, тем выше уровень точности, однако сопровождается этим увеличением точности также и увеличение времени выполнения.
Иногда в задаче классификации важно знать уверенность отнесения к тому или иному классу. В SVM за это отвечает параметр отступа (margin), который можно посчитать, используя model.decision_function(X). Именно отступ до разделяющей кривой пытается максимизироавть модель во время обучения. Однако по нему сложно утверждать с какой вероятностью объект относится к тому или иному классу.
Чтобы это понять давайте построим следующую кривую: 1) нормализуем отступы так, чтобы они лежали в диапазоне от 0 до 1;
2) разделим все объекты на бины по нормализованному оступу (например на [0, 0.1), [0.1, 0.2)...);
3) для каждого бина построим точку с координатой x - равной среднему значению нормализованного оступа внутри бина, и с координатой y - равной доле объектов класса 1 внутри бина.
Заметим теперь, что если бы нормализованный отступ приблизительно равнялся вероятности отнесения к классу, то бину [x, x + 0.1) должна была бы соответствовать точка с координатой Х принадлежащей [x, x + 0.1) и координатой Y в том же диапазоне. Таким образом, чем лучше скоры модели показывают вероятность отнесения к классу 1, тем больше калибровочная кривая похожа на прямую из точки (0, 0) в (1, 1).
Кроме того построим аналогично кривую для логистической регрессии, взяв вместо нормализованного отступа - вероятность отнесения к первому классу через predict_proba. Так как логистическая регрессия оптимизирует LogLoss и её выходом уже являются вероятности отнесения к классам, будем ожидать что её кривая хорошо ляжет на прямую из (0, 0) в (1, 1).
from sklearn.datasets import make_classification
from sklearn.calibration import calibration_curve
from sklearn.linear_model import LogisticRegression
# generate 2 class dataset
X, y = make_classification(n_samples=5000, n_classes=2, weights=[1,1], random_state=1)
# split into train/test sets
trainX, testX, trainy, testy = train_test_split(X, y, test_size=0.5, random_state=2)
# fit a model
model = SVC()
model.fit(trainX, trainy)
# predict probabilities
probs = model.decision_function(testX)
# reliability diagram
fop, mpv = calibration_curve(testy, probs, n_bins=10, normalize=True)
# plot perfectly calibrated
# plot model reliability
plt.plot(mpv, fop, marker='.', label='SVC')
plt.xlabel('classifier score')
plt.ylabel('[label=1] ratio')
logistic_model = LogisticRegression()
logistic_model.fit(trainX, trainy)
logistic_probs = logistic_model.predict_proba(testX)[:, 1]
# reliability diagram
log_fop, log_mpv = calibration_curve(testy, logistic_probs, n_bins=10, normalize=True)
# plot perfectly calibrated
# plot model reliability
plt.plot(log_mpv, log_fop, marker='.', label='logistic')
plt.plot([0, 1], [0, 1], linestyle='--', label='ideal')
plt.legend()
plt.show()
/usr/local/lib/python3.10/dist-packages/sklearn/calibration.py:1000: FutureWarning: The normalize argument is deprecated in v1.1 and will be removed in v1.3. Explicitly normalizing y_prob will reproduce this behavior, but it is recommended that a proper probability is used (i.e. a classifier's `predict_proba` positive class or `decision_function` output calibrated with `CalibratedClassifierCV`). warnings.warn( /usr/local/lib/python3.10/dist-packages/sklearn/calibration.py:1000: FutureWarning: The normalize argument is deprecated in v1.1 and will be removed in v1.3. Explicitly normalizing y_prob will reproduce this behavior, but it is recommended that a proper probability is used (i.e. a classifier's `predict_proba` positive class or `decision_function` output calibrated with `CalibratedClassifierCV`). warnings.warn(
Вот мы и увидели проблему, линия отступов не ложится на пунктирную линию, что означает что оступы не соотвествуют реальным вероятностям отнесения к тому или иному классу.
Но не всё пропало! Из отступов всё ещё можно получить вероятность отнесения к классу. Для этого существует такая процедура как калибровка вероятностей, при которой отступ для каждого объекта преобразовывается таким образом, чтобы соответствовать вероятности класса. После такого преобразования, полученное число становится интерпретируемой мерой уверенности модели.
В данном задании Вам прелагается обучить логистическую регрессию на отступах модели, которая по оступу (margin) предсказывала бы класс. Именно вероятности этой калибровочной модели и будут нашими верными оценками вероятности класса для объекта:
$p(y_i | x_i) = p(y_i | margin_i)$
Для этого Вам потребуется написать несложный класс CalibratingLogisticRegression. И проверить что новые предсказания дают правильную калибровочную кривую
class CalibratingLogisticRegression:
def fit(self, x, y):
assert len(x.shape) == 1 or x.shape[1] == 1
# Your code here
def predict_proba(self, x):
assert len(x.shape) == 1 or x.shape[1] == 1
# Your code here
Калибровочную модель и исходную модель нельзя учить на одних и тех же данных, чтобы избежать переобучения. (распределение отступов на обучении и тестовой выборке, скорее всего очень сильно отличается)
model = SVC()
N = len(trainX) // 2
model.fit(trainX[:N], trainy[:N])
SVC()In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
SVC()
margin = model.decision_function(trainX[N:])
# Учим калибровочную модель на второй половине данных
calibrating_model = CalibratingLogisticRegression()
calibrating_model.fit(margin, trainy[N:])
prob = calibrating_model.predict_proba(margin)
# Как это выглядит на обучении
margin_sorted, probs_sorted_by_margin = zip(*sorted(zip(margin, prob)))
plt.figure(figsize=(10, 10))
plt.plot(margin, trainy[N:], 'ro')
plt.plot(margin_sorted, probs_sorted_by_margin, 'b')
plt.show()
probs = calibrating_model.predict_proba(model.decision_function(testX))
# reliability diagram
fop, mpv = calibration_curve(testy, probs, n_bins=10, normalize=True)
# plot perfectly calibrated
plt.plot([0, 1], [0, 1], linestyle='--')
# plot calibrated reliability
plt.plot(mpv, fop, marker='.')
plt.show()
Обратите внимание, что кривая для откалиброванных отступов должня также строиться для бинов и состоянть из n_bins точек. Если у Вас получилось кривая из трёх точек, Вы что-то сделали неправильно :)
Сделайте выводы о полученной модели. В каких задачах калибровка вероятностей могла бы быть полезной?
Выводы: Полученная модель обладает способностью анализа отступов в качестве показателя вероятности отнесения к классу, что представляет отличие от исходной модели SVM. Это предоставляет удобство в ручной настройке порога уверенности, с которого мы принимаем решение относительно принадлежности объектов к определенному классу.
Найдите мем про SVM лучше чем этот:
Важно: самый простой способ вставить картинку будет через Google Colab (даже если вы изначально делали не в нем). Нажмите на "+ Text", в появившейся ячейке сделайте прикрепление картинки (как на скринах). Тогда ваша картинка "зашифруется" и будет корректно отображаться при конвертации в html